/**
 * 
 */
package info.niwota.frames;

import info.niwota.commons.io.Files;
import info.niwota.commons.web.ContentHelper;
import info.niwota.webapp.WebUtils;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.net.Uri.Builder;
import android.text.TextUtils;
import android.util.Log;

/**
 * 
 * frame name: [peer]_name
 * 
 * @author qiangli
 * 
 */
public final class FrameHelper {
	private static final int BUFSIZE = 1024;
	private static final String TAG = "FrameHelper";

	private static final String JSONP_LIST_FRAMES = "/content/info.niwota.frames/jsonp/frame/listFrames";
	private static final String JSONP_LOAD_FRAME = "/content/info.niwota.frames/jsonp/frame/loadFrame";
	private static final String BIN_IMAGE = "/content/info.niwota.frames/bin/image?data=true";
	private static final String BIN_ZIP = "/content/info.niwota.frames/bin/zip?data=true";
	
	private static final String STARRED = "Starred";
	
	private static final String PEER_STORE = "peer-store";
	private static final String PEER_FRAMES = "peer-frames";
	private static final String PEER_FRAME = "peer-frame";

	private FrameHelper() {
	}

	public static void save(List<Uri> uris, ImageFrame frame) {
		File file = frame.getStore();
		int pos = frame.getPosition();
		save(uris, file, pos);
	}
	
	synchronized private static void save(List<Uri> uris, final File file, final int pos) {
		if (uris == null) {
			return;
		}
		// uris.size() == 0 empty frame

		BufferedWriter out = null;
		try {
			out = new BufferedWriter(new FileWriter(file), BUFSIZE);

			out.write(pos + "");

			out.newLine();
			//
			for (Iterator<Uri> i = uris.iterator(); i.hasNext();) {
				Uri uri = i.next();
				out.write(uri.toString());
				out.newLine();
			}

			//for signaling changes to peers
			file.getParentFile().setLastModified(System.currentTimeMillis());
		} catch (Exception e) {

			e.printStackTrace();

		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

	private static Uri buildUri(String path) {
		Uri uri = Uri.parse("content://info.niwota.web" + path);
		return uri;
	}

	public static JSONObject getPeerFrame(Context ctx, String peer,
			String label, long lastModified) throws Exception {

		//final File file = getPeerStore(ctx, peer, label);
		final File file = cachedPeerFrame(ctx, peer, label);
		
		if (Debug.DEBUG) {
			Log.d(TAG, "getPeerFrame cached file: " + file + " file ts: " + file.lastModified() + " lastModified: " + lastModified);
		}
		
		if (file.exists() && file.length() > 0) {
			try {
				JSONObject jo = ContentHelper.readObject(file);
				//
				long ts = jo.getLong("lastModified");
				
				if (Debug.DEBUG) {
					Log.d(TAG, "getPeerFrame json lastModified: " + ts);
				}
				
				if (ts == lastModified) {
					return jo;
				}
			} catch (Exception e) {
				e.printStackTrace();

				file.delete();
			}
		}

		String path = JSONP_LOAD_FRAME + "?format=text/plain&peer=" + peer
				+ "&label=" + WebUtils.encodeURIComponent(label);
		
		final Uri cu = buildUri(path);
		if (Debug.DEBUG) {
			Log.d(TAG, "getPeerFrame download from peer: " + peer + " label: " + label
					+ " path: " + path + " cu: " + cu);
		}
		String s = ContentHelper.getContent(ctx, cu);
		
		if (s.startsWith("STATUS-5") || s.startsWith("STATUS-4")) {
			throw new Exception(s);
		}
		
		//clear cached thumbnails
		deleteFiles(thumbFolder(ctx, peer, label));
		
		//cache
		JSONObject jo = new JSONObject(s);
		
		ContentHelper.saveObject(jo, file);
		
		long ts = jo.getLong("lastModified");
		file.setLastModified(ts);
		
		return jo;
	}

	public static void deleteFiles(File file) {
		if (file.isFile()) {
			file.delete();
			return;
		}
		File[] files = file.listFiles();
		if (files == null) {
			return;
		}
		for (int i = 0; i < files.length; i++) {
			deleteFiles(files[i]);
		}
	}
	
	public static void touch(File file, long now) {
		file.setLastModified(now);
		if (file.isDirectory()) {
			File[] files = file.listFiles();
			for (int i = 0; i < files.length; i++) {
				touch(files[i], now);
			}
		}
	}

	public static Uri buildPeerImageContentUri(String peer, String label, String hash, String imgUri) {
		String base = BIN_IMAGE + "&peer=" + peer + "&label="
			+ WebUtils.encodeURIComponent(label);	
		String imglink = base + "&hash=" + hash + "&uri=" + WebUtils.encodeURIComponent(imgUri);
		Uri cu = buildUri(imglink);
		return cu;
	}
	
	private static int loadPeer(Context ctx, List<Uri> uris, ImageFrame frame)
			throws Exception {

		final String[] sa = frame.getPeerLabel();
		final String peer = sa[0];
		final String label = sa[1];

		JSONObject jo = getPeerFrame(ctx, peer, label, frame.getLastModified());
		
		final int total = jo.getInt("total");
		int pos = 0;
		JSONArray ja = jo.getJSONArray("list");
		final int len = ja.length();

		if (total != len) {
			throw new IOException("JSON File corrupted " + len + " total: "
					+ total + " json: " + jo);
		}

		for (int i = 0; i < len; i++) {
			try {
				JSONObject jai = ja.getJSONObject(i);
				String imgUri = jai.getString("uri");
				String hash = jai.getString("hash");
				
				Uri cu = buildPeerImageContentUri(peer, label, hash, imgUri);
				if (Debug.DEBUG) {
					Log.d(TAG, "adding " + cu);
				}
				uris.add(cu);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return pos;
	}

	/**
	 * Load from local file or from peer
	 * 
	 * @param ctx
	 * @param uris
	 * @param frame
	 * @return
	 * @throws Exception
	 */
	synchronized public static int load(Context ctx, List<Uri> uris,
			ImageFrame frame) throws Exception {
		int pos = 0;
		if (frame.isPeer()) {
			pos = loadPeer(ctx, uris, frame);
		} else {
			final File file = frame.getStore();
			pos = load(uris, file);
		}
		frame.setPosition(pos);
		return pos;
	}

	/**
	 * Load from local file
	 * @param uris
	 * @param file
	 * @return saved position
	 */
	private static int load(List<Uri> uris, File file) {
		// header: pos
		int pos = 0;
		BufferedReader in = null;
		try {
			in = new BufferedReader(new FileReader(file), BUFSIZE);
			String line = in.readLine();
			if (line != null) {
				try {
					pos = Integer.parseInt(line);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			while ((line = in.readLine()) != null) {
				try {
					Uri uri = Uri.parse(line);
					uris.add(uri);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		//validate position
		int cnt = uris.size();
		return (cnt == 0 ? 0 : cnt <= pos ? cnt - 1 : pos);
	}

	/**
	 * List my frames
	 * 
	 * @param ctx
	 * @param root
	 * @return
	 */
	public static File[] list(Context ctx) {
		File root = getStoreRoot(ctx);

		File[] files = root.listFiles();
		if (files == null || files.length == 0) {
			Util.newFile(ctx, root, ImageFrame.DEFAULT_FRAMENAME);
			files = root.listFiles();
		}
		Arrays.sort(files);
		return files;
	}

	public static boolean isEZBoardingAvailable(Context ctx) {
		final PackageManager pm = ctx.getPackageManager();
		final Intent i = createEZBoardingIntent();
		List<ResolveInfo> l = pm.queryIntentActivities(i,
				PackageManager.MATCH_DEFAULT_ONLY);
		return (l.size() > 0);
	}

	public static Intent createEZBoardingIntent() {
		final Intent i = new Intent(Intent.ACTION_GET_CONTENT);
		i.setClassName("info.niwota.ezb", "info.niwota.ezb.ImageHolderActivity");
		return i;
	}

	private static File getStoreRoot(Context ctx) {
		return ctx.getDir("frames", Context.MODE_PRIVATE);
	}

	public static File getPermStore(Context ctx) {
		final String name = "labels.acl";
		File dir = ctx.getFilesDir();
		File f = new File(dir, name);
		if (!f.exists()) {
			try {
				FileOutputStream os = ctx.openFileOutput(name,
						Context.MODE_PRIVATE);
				os.close();
			} catch (Exception e) {

				e.printStackTrace();
			}
		}
		return f;
	}

	public static JSONObject getPeerFrames(Context ctx, String peer)
			throws Exception {
		//check for update
		final String querypath = JSONP_LIST_FRAMES + "?info=true&format=text/plain&peer=" + peer;
		final Uri querycu = buildUri(querypath);
		
		if (Debug.DEBUG) {
			Log.d(TAG, "getPeerFrames peer: " + peer + " querypath: " + querypath+ " querycu: " + querycu);
		}
		
		String rs = ContentHelper.getContent(ctx, querycu);
		if (rs.startsWith("STATUS-5") || rs.startsWith("STATUS-4")) {
			throw new Exception(rs);
		}
		
		JSONObject jo = new JSONObject(rs);
		long lastModified = jo.getLong("lastModified");
		
		File fo = cachedPeerFrames(ctx, peer);
		
		if (fo.exists() && fo.lastModified() > lastModified) {
			if (Debug.DEBUG) {
				Log.d(TAG, "getPeerFrames returning cache. peer: " + peer +" lastModified: " +  lastModified + " local: " + fo.lastModified());
			}
			return ContentHelper.readObject(fo);
		}
		
		//
		final String path = JSONP_LIST_FRAMES + "?format=text/plain&peer=" + peer;
		final Uri cu = buildUri(path);
		
		if (Debug.DEBUG) {
			Log.d(TAG, "getPeerFrames peer: " + peer + " uri: " + path
					+ " cu: " + cu);
		}
		
		rs = ContentHelper.getContent(ctx, cu);
		if (rs.startsWith("STATUS-5") || rs.startsWith("STATUS-4")) {
			throw new Exception(rs);
		}
		jo = new JSONObject(rs);
		
		//cache
		ContentHelper.saveObject(jo, fo);
		
		return jo;
	}

	private static File cachedPeerFrames(Context ctx, String peer) {
		File dir = ctx.getDir(PEER_FRAMES, Context.MODE_PRIVATE); 
		File fo = new File(dir, peer);
		return fo;
	}

	private static ImageFrame loadLocalFrame(Context ctx, File file) throws Exception {
		ImageFrame frame = new ImageFrame();
		frame.setStore(file);
		List<Uri> uris = new ArrayList<Uri>();
		load(ctx, uris, frame);
		frame.setReferences(uris);
		frame.setLastModified(file.lastModified());
		return frame;
	}

	public static File getLocalStore(Context ctx, String label) throws IOException {
		File root = getStoreRoot(ctx);
		File file = new File(root, label);

		if (!file.exists()) {
			throw new IOException("Not found.");
		}

		return file;
	}

	synchronized public static File getCachedPeerThumbnail(Context ctx, String peer,
			String label, int pos) throws Exception {
		
		File thumbfolder = thumbFolder(ctx, peer, label);
		
		File cachefile = new File(thumbfolder, ""+pos);
		
		if (cachefile.exists()) {
			if (Debug.DEBUG) {
				Log.d(TAG, "return cached thumb: " + cachefile);
			}
			return cachefile;
		}

		final int batch = ImageFrame.getBatchNo(pos);
		
		File zipfile = new File(thumbfolder, batch + ".zip");

		// /content/info.niwota.frames/bin/zip?label=Pictures&batch=0&size=thumbnail
		Uri base = Uri.parse(BIN_ZIP);
		Builder b = base.buildUpon();
		b.appendQueryParameter("peer", peer);
		b.appendQueryParameter("format", "text/plain");
		b.appendQueryParameter("label", WebUtils.encodeURIComponent(label));
		b.appendQueryParameter("batch", "" + batch);
		b.appendQueryParameter("size", "thumbnail");
		b.appendQueryParameter("cache", "false");

		String path = b.toString();
		final Uri cu = buildUri(path);
		if (Debug.DEBUG) {
			Log.d(TAG, "getCachedPeerThumbnail downloading... peer: " + peer
					+ " label: " + label + " path: " + path + " cu: " + cu);
		}

		thumbfolder.mkdirs();
		
		// download
		try {
			ContentHelper.saveContent(ctx, cu, zipfile);
		} catch (Exception e) {
			zipfile.delete();
			throw e;
		}

		// unzip
		if (Debug.DEBUG) {
			Log.d(TAG, "getCachedPeerThumbnail unzipping... peer: " + peer
					+ " label: " + label + " zipfile: " + zipfile);
		}
		unzip(thumbfolder, zipfile);
		
		zipfile.delete(); //
		
		if (!cachefile.exists()) {
			
			throw new FileNotFoundException(peer + "/" + label +" cache: " + cachefile);
		}

		return cachefile;
	}

	private static File thumbFolder(Context ctx, String peer, String label) {
		final String top = "/download/thumb/peer/" + peer + "/" + encodeLabel(label);
		File cache = cacheDir(ctx);
		return new File(cache, top);
	}

	private static File cachedPeerFrame(Context ctx, String peer, String label) {
		File dir = ctx.getDir(PEER_FRAME, Context.MODE_PRIVATE); 
		File file = new File(dir, "~" + peer + " " + encodeLabel(label) + ".json");
		return file;
	}
	
//	private static String cachedPeerThumbFolder(String peer, String label) {
//		return 
//	}

	private static void unzip(File top, File zip) throws Exception {
		try {
			FileInputStream fis = new FileInputStream(zip);
			ZipInputStream zis = new ZipInputStream(fis);
			ZipEntry ze = zis.getNextEntry();
			while (ze != null) {
				if (!ze.isDirectory()) {
					String name = ze.getName();
					
					if (Debug.DEBUG) {
						Log.d("TAG", " unzip: " + name);
					}
					
					File f = new File(top, name);
					File dir = f.getParentFile();
					if (dir != null && !dir.exists()) {
						dir.mkdirs();
					}
					FileOutputStream fos = new FileOutputStream(f);
					int n = 0;
					final byte[] buf = new byte[1024];
					while ((n = zis.read(buf, 0, 1024)) > -1) {
						fos.write(buf, 0, n);
					}
					fos.close();
					long modified = ze.getTime();
					f.setLastModified(modified);
				}
				zis.closeEntry();
				ze = zis.getNextEntry();
			}
			zis.close();
		} catch (Exception e) {
			throw e;
		}
	}
	
	public static void addToStarred(Context ctx, List<Uri> uris) throws Exception {
		if (uris == null || uris.size() == 0) {
			return;
		}
		File store = ensureStarStore(ctx);
		
		ImageFrame frame = new ImageFrame();
		frame.setStore(store);
		frame.restore(ctx);
			
		for (Uri uri: uris) {
			final int pos = frame.getPosition();
			frame.add(ctx.getContentResolver(), pos, uri);
		}
		frame.save(ctx);
		frame.clear();
	}

	public static File ensureStarStore(Context ctx) {
		File root = getStoreRoot(ctx);
		File file = new File(root, STARRED);
		try {
			if (file.exists()) {
				return file;
			}
			File p = file.getParentFile();
			if (!p.exists()) {
				p.mkdirs();
			}
			FileOutputStream os = new FileOutputStream(file);
			os.close();
		} catch (Exception e) {
			//
		}
		return file;
	}

	public static File getPeerStore(Context ctx, String peer, String label) {
		File dir = ctx.getDir(PEER_STORE, Context.MODE_PRIVATE);
		File f = Util.ensurePeerFile(ctx, dir, peer, label);
		return f;
	}

	public static File createStore(Context ctx) {
		File root = getStoreRoot(ctx);
		return Util.newFile(ctx, root, ImageFrame.DEFAULT_FRAMENAME);
	}

	public static File createStore(Context ctx, String name) {
		File root = getStoreRoot(ctx);
		return Util.newFile(ctx, root, name);
	}

	public static ImageFrame loadLocalFrame(Context ctx, String label) throws Exception {
		File store = getLocalStore(ctx, label);
		return loadLocalFrame(ctx, store);
	}
	
//	public static ImageFrame loadFrame(Context ctx, String label) throws Exception {
//		File store = getLocalStore(ctx, label);
//		return loadFrame(ctx, store);
//	}

	public static JSONObject queryFrames(Context ctx) throws JSONException {
		//only modified timestamp for now
		JSONObject jo = new JSONObject();
		File root = getStoreRoot(ctx);
		jo.put("lastModified", root.lastModified());
		return jo;
	}

	public static File getCachedPeerImage(Context ctx, Uri uri, long modified) throws Exception {
		String peer = uri.getQueryParameter("peer");
		String label = uri.getQueryParameter("label");
		if (TextUtils.isEmpty(peer) || TextUtils.isEmpty(label)) {
			throw new Exception("Missing peer or label: " + uri);
		}
		
		final String top = "/download/vignette/peer/" + peer + "/" + encodeLabel(label);
		
		File cache = cacheDir(ctx);
		File cachefile = new File(cache, top + "/" + WebUtils.validPathname(uri.getQueryParameter("uri"), true));
		
		if (Debug.DEBUG) {
			Log.d(TAG, "getCachedPeerImage cachefile: " + cachefile);
		}
		
		if (cachefile.exists()) {
			if (Debug.DEBUG) {
				Log.d(TAG, "return cached vignette: " + cachefile);
			}
			return cachefile;
		}
		
		Builder b = uri.buildUpon();
		//?peer=&label=&hash=&uri=
		b.appendQueryParameter("format", "text/plain"); //response format if error
		b.appendQueryParameter("size", "vignette");
		b.appendQueryParameter("cache", "false"); 
		b.appendQueryParameter("data", "true");
		
		Uri u = b.build();
		
		ContentHelper.saveContent(ctx, u, cachefile);
		
		return cachefile;
	}
	
	private static File cacheDir(Context ctx) {
		return Files.getCacheDir(ctx);
	}

	synchronized public static void add(Uri uri, File file) {
		ArrayList<Uri> uris = new ArrayList<Uri>();
		int pos = load(uris, file);
		uris.add(pos, uri);
		save(uris, file, pos);
	}
	
	synchronized public static void add(List<Uri> nuris, File file) {
		ArrayList<Uri> uris = new ArrayList<Uri>();
		int pos = load(uris, file);
		for (Uri uri: nuris) {
			uris.add(pos, uri);
		}
		save(uris, file, pos);
	}
	
	public static String encodeLabel(String label) {
		//TODO could be a problem
		return label.replace('*', '.');
	}

}
